今天就來實作圖片上傳的功能及端點!
一開始到 core/models.py 中,因為目前的 Book model 並沒有圖片的欄位,需要先增加欄位並且指定上傳路徑:
import os
import uuid
# ...
def book_image_file_path(instance, filename):
    """Generate file path for new book image."""
    ext = os.path.splitext(filename)[1]
    filename = f'{uuid.uuid4()}{ext}'
    return os.path.join('uploads', 'book', filename)
class Book(models.Model):
    # ...
    image = models.ImageField(null=True, upload_to=book_image_file_path)
下方的 image 欄位中指定的upload_to引數會對應到上面的函式,函式內的ext變數會被賦予該檔案的格式(os.path.splitext()會將檔案切成陣列,陣列中[1]即為檔案格式)。接著使用 uuid 產生一組獨特的字元做為新的檔案名。最後會將檔案儲存在uploads/book/中。此函式的目的為將原本檔案轉換為獨特值並保留檔案格式,避免出現重複的情況。
完成後記得進行資料庫更新:
python manage.py makemigrations
python manage.py migrate
接著來進行 serializer 的製作,進入 book/serializer.py 並修改及新增以下程式碼:
# ...
class BookDetailSerializer(BookSerializer):
    """Serializer for book detail view."""
    class Meta(BookSerializer.Meta):
        fields = BookSerializer.Meta.fields + ['description', 'image'] # 新增 image field
class BookImageSerialzer(serializers.ModelSerializer):
    """Serializer for uploading images to books."""
    class Meta:
        model = Book
        fields = ['id', 'image']
        read_only_fields = ['id']
        extra_kwargs = {'image': {'required': True}}
BookDetailSerializer中增加 image,讓 image 也可顯示在回應中。新增的BookImageSerialzer裡面指定 id 跟 image 欄位進行序列化,並指定 image 為必填欄位。完成後進入 book/views.py ,要再次修改get_serializer_class、新增導入項目以及新增程式碼如下:
from rest_framework import viewsets, status # 新導入 status
from rest_framework.decorators import action
from rest_framework.response import Response
# ...
class BookViewSet(viewsets.ModelViewSet):
    # ...
    def get_serializer_class(self):
        """Return the serializer class for request."""
        if self.action == 'list':
            return serializers.BookSerializer
        elif self.action == 'upload_image':
            return serializers.BookImageSerialzer
        return self.serializer_class
    @action(methods=['POST'], detail=True, url_path='upload_image')
    def upload_image(self, request, pk=None):
        """Upload an image to book."""
        book = self.get_object()
        serializer = self.get_serializer(book, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_200_OK)
        return Response(serializer.error, status=status.HTTP_400_BAD_REQUEST)
這邊在get_serializer_class新增上傳圖片的條件。但預設 action 並無 upload_image,所以需要自定義在下方。函式上方的裝飾器裡,methods=['POST']表示只接受 POST 請求,detail=True表示此功能僅作用在特定一物件上;若detail=False則會作用在所有物件,整體來說,若要觸發 POST, API 請求中的 URL 需要包含書籍的主鍵(pk)和 'upload_image' 路徑,例如 /books/1/upload_image/。
函式內部首先先用get_object()取得該物件,並用此物件及請求中的資料初始化 serializer(就是呼叫get_serializer_class()),再來驗證資料正確性,若正確儲存資料並回傳 200;反之則回傳 400。
以上儲存後,到 rest_api/settings.py 中新增:
# ...
SPECTACULAR_SETTINGS = {
    'COMPONENT_SPLIT_REQUEST': True,
}
這樣在上傳檔案時才會出現選擇檔案的按鈕。
都儲存後,就打開伺服器測試看看吧
在 Swagger 介面中,
upload_image的端點要使用multipart/formdata才能正確上傳
到這邊,book RESTful API 算是完成了,而本次鐵人賽所有的作品也如期完成,最後來個小總結,明天見~